﻿using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Resources;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.CommandBars;

namespace VsTortoiseSVN
{
    public static class CommandBarControlExtensionMethods
    {
        /// <summary>
        /// Gets the Command where the specified control is associated to.
        /// </summary>
        /// <returns>Returns the Command on success, otherwise null.</returns>
        public static Command GetCommand(this CommandBarControl me)
        {
            Command command = null;

            try
            {
                int id;
                string guid;
                DTE2 dte = me.Application as DTE2;

                dte.Commands.CommandInfo(me, out guid, out id);
                command = dte.Commands.Item(guid, id);
            }
            catch
            { }

            return command;
        }
    }

    public static class CommandBarExtensionMethods
    {
        /// <summary>
        /// Gets the CommandBarButton from the specified commandBarPopup, which is
        /// associated to a Command with the name specified by commandName.
        /// </summary>
        /// <returns>Returns the CommandBarButton on success, otherwise null.</returns>
        public static CommandBarButton GetButton(this CommandBar me, string commandName)
        {
            // http://social.msdn.microsoft.com/Forums/en-US/vsx/thread/65bb4595-240e-4147-94d4-4fd762476eb2
            foreach (CommandBarControl control in me.Controls)
            {
                if (control is CommandBarButton)
                {
                    Command command = control.GetCommand();
                    if (command != null)
                    {
                        if (string.Compare(command.Name, commandName) == 0)
                            return control as CommandBarButton;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Gets the best matching "VsTortoise" caption, respecting acceleration keys of other popup menus.
        /// I thought this voodoo makes sense, because acceleration keys are different
        /// in every language, thus it's impossible for me to come up with one handy acceleration key
        /// for VsTortoise that works with every language.
        /// </summary>
        public static string GetBestMatchingVsTortoiseCaption(this CommandBar me)
        {
            // Builds a list of preferred acceleration keys for the VsTortoise caption.
            // Criteria is to prefer keys at the left side of the keyboard, since this is where
            // the Alt key is located, thus easy to execute with one hand only.
            // If an acceleration key is used by another popup menu already, we go one entry/index
            // further in the list. If all acceleration keys are in use already, we fall back to
            // "V&sTortoise" which is shared with another popup menu then.
            List<KeyValuePair<string, string>> preferredAccelerators = new List<KeyValuePair<string, string>>();
            preferredAccelerators.Add(new KeyValuePair<string, string>("&s", "V&sTortoise"));
            preferredAccelerators.Add(new KeyValuePair<string, string>("&e", "VsTortois&e"));
            preferredAccelerators.Add(new KeyValuePair<string, string>("&r", "VsTo&rtoise"));
            preferredAccelerators.Add(new KeyValuePair<string, string>("&t", "Vs&Tortoise"));
            preferredAccelerators.Add(new KeyValuePair<string, string>("&v", "&VsTortoise"));
            preferredAccelerators.Add(new KeyValuePair<string, string>("&i", "VsTorto&ise"));
            preferredAccelerators.Add(new KeyValuePair<string, string>("&o", "VsT&ortoise"));

            // We start at the first entry
            int preferredIndex = 0;

            int n = 1;
            while (n < me.Controls.Count)
            {
                CommandBarControl control = me.Controls[n++];
                if (control is CommandBarPopup)
                {
                    // Check if the accelerator is used by another control already
                    if (control.Visible && control.Caption.IndexOf(preferredAccelerators[preferredIndex].Key, StringComparison.OrdinalIgnoreCase) > -1)
                    {
                        // It is used by another control already.
                        // Move over to our next preferred accelerator key and start
                        // the search at the very beginning again.

                        n = 1; // start all over again

                        if (++preferredIndex >= preferredAccelerators.Count)
                            break; // all preferred keys used already
                    }
                }
            }

            // Fall back to "V&sTortoise" and only use the preferred
            // accelerator if one was found.
            string caption = "V&sTortoise";
            if (preferredIndex < preferredAccelerators.Count)
                caption = preferredAccelerators[preferredIndex].Value;

            return caption;
        }
    }

    public static class Commands2ExtensionMethods
    {
        /// <summary>
        /// Gets the Command specified by shortName..
        /// </summary>
        /// <param name="shortName">Command short name</param>
        /// <returns>Returns the Command on success or null when the Command does not exist.</returns>
        public static Command GetCommand(this Commands2 me, string fullname)
        {
            Command command = null;

            // Try to retrieve the command, ignore any exception that would
            // occur if the command was not created yet.
            try
            {
                command = me.Item(fullname, -1);
            }
            catch
            { }

            return command;
        }

        public static Command GetCommandByBinding(this Commands2 me, string binding)
        {
            if (!string.IsNullOrEmpty(binding))
            {
                foreach (Command command in me)
                {
                    object[] bindings = (object[])command.Bindings;

                    foreach (object b in bindings)
                    {
                        if (string.Compare(binding, b as string, true) == 0)
                            return command;
                    }
                }
            }

            return null;
        }

        public static string GetFullname(this Commands2 me, AddIn addin, string shortName)
        {
            return string.Format("{0}.{1}", addin.ProgID, shortName);
        }

        public static Command CreateCommandButton(this Commands2 me, AddIn addin, VsTortoiseSVN.Commands.CommandBase command)
        {
            object[] contextGUIDS = new object[] { };
            Commands2 commands = me;

            // Try to retrieve the command. If it was created already, just leave since the command exists.
            Command vsCommand = me.GetCommand(me.GetFullname(addin, command.Name));
            if (vsCommand != null)
                return null; // it already exists

            try
            {
                // Try to create a command with icon. 
                // This usually fails using Visual Studio 2005 if the VsTortoise satellite dll culture does not match the one of Visual Studio.
                vsCommand = commands.AddNamedCommand2(addin, command.Name, command.Caption, command.Tooltip, false, command.BitmapId, 
                    ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled,
                    (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);
            }
            catch
            { }

            if (vsCommand == null)
            {
                // We're here when command creation failed, so create it without icon.
                vsCommand = commands.AddNamedCommand2(addin, command.Name, command.Caption, command.Tooltip, true, (int)0, 
                    ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled, 
                    (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);
            }

            if (vsCommand != null)
            {
                // First check if there is a preferred binding at all,
                // to skip unnecessary further expensive tests
                if (!string.IsNullOrEmpty(command.PreferredBinding))
                {
                    // The command inside VS has been added.
                    // Now check if any command is using the preferred binding already.
                    // We use it only, when it is not used by another command!
                    // Bindings are "atomic". If we would use a binding that is in use already,
                    // the binding gets removed from the current command. We don't want to steal bindings from other commands!
                    bool bindingUsedAlready = me.GetCommandByBinding(command.PreferredBinding) != null;
                    if (!bindingUsedAlready)
                    {
                        try
                        {
                            // TODO: figure how to assign bindings correctly
                            // Bindings use translated texts, this is so ridiculous.
                            // I don't know how to assign a shortcut in a language independent way,
                            // so we use a try/catch here, otherwise the add-in refuses to start-up.
                            // English: "Text Editor::Ctrl+Shift+Alt+Up Arrow"
                            // German.: "Text-Editor::Strg+Umschalt+Alt+NACH-OBEN-TASTE"
                            vsCommand.Bindings = command.PreferredBinding;
                        }
                        catch
                        { }
                    }
                }
            }

            return vsCommand;
        }
    }

    public static class DTE2ExtensionMethods
    {
        /// <summary>
        /// Gets whether a document is active and exists on the file system.
        /// </summary>
        public static bool HasDocument(this DTE2 dte)
        {
            if (dte.ActiveDocument == null)
                return false;

            return !string.IsNullOrEmpty(dte.ActiveDocument.FullName);
        }

        /// <summary>
        /// Gets whether a solution file is active and exists on the file system.
        /// </summary>
        public static bool HasSolution(this DTE2 dte)
        {
            if (dte.Solution == null)
                return false;

            return dte.Solution.IsOpen && !string.IsNullOrEmpty(dte.Solution.FullName);
        }

        /// <summary>
        /// Gets whether a project file is active that exists on the file system.
        /// </summary>
        public static bool HasProject(this DTE2 dte)
        {
            if (!dte.HasSolution())
                return false;

            return dte.ActiveSolutionProjects != null &&
                dte.Solution.Projects != null && dte.Solution.Projects.Count > 0;
        }

        public static Project ActiveProject(this DTE2 dte)
        {
            Array projects = dte.ActiveSolutionProjects as Array;
            if(projects.Length > 0)
                return projects.GetValue(0) as Project;

            return null;
        }

        /// <summary>
        /// Saves all currently opened documents.
        /// </summary>
        public static bool SaveDocuments(this DTE2 dte)
        {
            foreach (EnvDTE.Document document in dte.Documents)
            {
                if (!document.Save2())
                    return false;
            }

            return true;
        }
    }


    public static class SolutionExtensionMethods
    {
        public static string LongFullName(this EnvDTE.Solution solution)
        {
            return PathHelper.GetLongPathName(solution.FullName);
        }

        public static void GetDirectoryNames(this EnvDTE.Solution solution, ICollection<string> dest)
        {
            // Add Solution directory to list
            dest.Add(Path.GetDirectoryName(solution.LongFullName()));

            // Add all Project directories to list
            for (int n = 1; n <= solution.Projects.Count; ++n)
            {
                Project project = solution.Projects.Item(n);
                if (!string.IsNullOrEmpty(project.FullName))
                    project.GetDirectoryNames(dest);
            }
        }
    }

    public static class ProjectExtensionMethods
    {
        public static string LongFullName(this EnvDTE.Project project)
        {
            string path = PathHelper.GetLongPathName(project.FullName);
            return path;
        }

        private static void GetDirectoryNamesRecursive(this EnvDTE.Project project, Dictionary<string, int> folders, ProjectItem projectItem)
        {
            // Check if this is a physically existing file
            // as we are only interested in this.
            if (string.Compare(projectItem.Kind, Constants.vsProjectItemKindPhysicalFile, true) == 0)
            {
                string filename = projectItem.get_FileNames(0);
                string directory = Path.GetDirectoryName(filename);

                if (!folders.ContainsKey(directory))
                    folders.Add(directory, 1);
            }

            // If this item has sub-items, process them too
            foreach (ProjectItem item in projectItem.ProjectItems)
            {
                GetDirectoryNamesRecursive(project, folders, item);
            }
        }

        public static void GetDirectoryNames(this EnvDTE.Project project, ICollection<string> dest)
        {
            // the dictionary is used as fast approach to
            // look up if a directory has been added to the list already
            Dictionary<string, int> folders = new Dictionary<string, int>(128, StringComparer.OrdinalIgnoreCase);

            // Check each project item
            foreach (ProjectItem projectItem in project.ProjectItems)
                GetDirectoryNamesRecursive(project, folders, projectItem);

            // Add folders to destination
            foreach (string folder in folders.Keys)
                dest.Add(PathHelper.GetLongPathName(folder));
        }
    }

    public static class ProjectItemExtensionMethods
    {
        /// <summary>
        /// Gets whether the ProjectItem is a physical file.
        /// </summary>
        public static bool IsFile(this EnvDTE.ProjectItem me)
        {
            return string.Compare(me.Kind, Constants.vsProjectItemKindPhysicalFile, true) == 0;
        }

        /// <summary>
        /// Gets whether the ProjectItem is a physical folder.
        /// </summary>
        public static bool IsFolder(this EnvDTE.ProjectItem me)
        {
            return string.Compare(me.Kind, Constants.vsProjectItemKindPhysicalFolder, true) == 0;
        }

        /// <summary>
        /// Gets whether the ProjectItem is a Filter. A Filter is a virtual folder that
        /// only exists inside a Visual Studio Project, but not on the file system.
        /// </summary>
        public static bool IsFilter(this EnvDTE.ProjectItem me)
        {
            return string.Compare(me.Kind, Constants.vsProjectItemKindVirtualFolder, true) == 0;
        }

        public static int GetLongFullNames(this EnvDTE.ProjectItem me, ICollection<string> dest)
        {
            int beforeCount = dest.Count;

            if (me.IsFile() || me.IsFolder())
            {
                // This item is a physical file or folder
                for (int n = 0; n < me.FileCount; ++n)
                {
                    string fileName = me.get_FileNames((short)(1 + n));
                    string longName = PathHelper.GetLongPathName(fileName);

                    if (!string.IsNullOrEmpty(longName))
                        dest.Add(longName);
                }
            }
            else if (me.IsFilter())
            {
                // A Filter can be found in C++ projects for example.
                // Filters are Virtual Folders, they appear inside a project as
                // folder but don't exist on the file-system. C# projects don't seem
                // to support filters, but folders.
                for (int n = 0; n < me.ProjectItems.Count; ++n)
                {
                    // A filter can contain files and even sub-filters.
                    // Get filenames recursivly from all sub items.
                    EnvDTE.ProjectItem subItem = me.ProjectItems.Item(1 + n);
                    subItem.GetLongFullNames(dest);
                }
            }
            else
            {
                Output.Instance.WriteLine(string.Format("Unknown ProjectItem: Name={0}, Kind={1}", me.Name, me.Kind));
            }

            return dest.Count - beforeCount;
        }
    }

    public static class DocumentExtensionMethods
    {
        public static string LongFullName(this EnvDTE.Document document)
        {
            return PathHelper.GetLongPathName(document.FullName);
        }

        public static bool Save2(this EnvDTE.Document document)
        {
            if (document.Saved)
                return true; // not modified

            string filename = document.LongFullName();
            bool success = document.Save(filename) == vsSaveStatus.vsSaveSucceeded;
            return success;
        }
    }

    public static class TextSelectionExtensionMethods
    {
        public static void GotoLineCentered(this EnvDTE.TextSelection selection, int line)
        {
            int offset = selection.TextPane.Height / 2;

            if (line > selection.TextPane.StartPoint.Line + selection.TextPane.Height)
            {
                selection.GotoLine(line + offset, false);
                selection.LineUp(false, offset);
            }
            else if (line < selection.TextPane.StartPoint.Line)
            {
                selection.GotoLine(line - offset, false);
                selection.LineDown(false, offset);
            }
            else
            {
                selection.GotoLine(line, false);
            }
        }
    }

    public enum SelectedItemKind
    {
        File,
        Folder,
        Filter,
        Project,
        Solution,
        Unknown,
    }

    public static class SelectedItemExtensionMethods
    {
        public static bool IsFile(this EnvDTE.SelectedItem me)
        {
            if (me.ProjectItem == null)
                return false;

            return me.ProjectItem.IsFile();
        }

        public static bool IsFolder(this EnvDTE.SelectedItem me)
        {
            if (me.ProjectItem == null)
                return false;

            return me.ProjectItem.IsFolder();
        }

        public static bool IsFilter(this EnvDTE.SelectedItem me)
        {
            if (me.ProjectItem == null)
                return false;

            return me.ProjectItem.IsFilter();
        }

        public static bool IsProject(this EnvDTE.SelectedItem me)
        {
            if (me.Project != null && me.ProjectItem == null)
                return true;

            return false;
        }

        public static bool IsSolution(this EnvDTE.SelectedItem me)
        {
            if (me.ProjectItem == null && me.Project == null)
                return true;

            return false;
        }

        public static SelectedItemKind GetKind(this EnvDTE.SelectedItem me)
        {
            if (me.IsFile())
                return SelectedItemKind.File;

            if (me.IsFolder())
                return SelectedItemKind.Folder;

            if (me.IsFilter())
                return SelectedItemKind.Filter;

            if (me.IsProject())
                return SelectedItemKind.Project;

            if (me.IsSolution())
                return SelectedItemKind.Solution;

            return SelectedItemKind.Unknown;
        }

        public static int GetFullNames(this EnvDTE.SelectedItem me, ICollection<string> dest)
        {
            int beforeCount = dest.Count;

            switch (me.GetKind())
            {
                case SelectedItemKind.File:
                case SelectedItemKind.Folder:
                case SelectedItemKind.Filter:
                    me.ProjectItem.GetLongFullNames(dest);
                    break;

                case SelectedItemKind.Project:
                    me.Project.GetDirectoryNames(dest);
                    break;

                case SelectedItemKind.Solution:
                    me.DTE.Application.Solution.GetDirectoryNames(dest);
                    break;
            }

            int addedCount = dest.Count - beforeCount;
            return addedCount;
        }
    }
}
